﻿using System;
using System.Collections.Generic;
using System.IO;

namespace Red.Toolkit.Profiler.Logic
{
    public class ProfilerDataLoader
    {
        public ProfilerData LoadFromFile( string path )
        {
            try
            {
                using ( var br = new FileStream( path, FileMode.Open, FileAccess.Read, FileShare.Read, 0x10000, FileOptions.SequentialScan ) )
                {
                    byte[] buffer = new byte[br.Length];
                    br.Read( buffer, 0, buffer.Length );
                    using ( var memoryStream = new MemoryStream( buffer ) )
                    {
                        using ( BinaryReader b = new BinaryReader( memoryStream ) )
                        {
                            return LoadFromFile( path, b );
                        }
                    }
                }
            }
            catch ( Exception )
            {
            }

            return null;
        }

        public ProfilerData LoadFromFile( string path, BinaryReader reader )
        {
            // load and verify header
            uint mainThread = 0;
            ulong timeBase = 0;
            ulong timeFreq = 0;
            ulong engineInstanceId = 0;
            try
            {
                uint magic = reader.ReadUInt32();
                uint version = reader.ReadUInt32();

                if ( version < 1 || version > 2 )
                    throw new InvalidDataException( "Unsupported file version" );

                mainThread = reader.ReadUInt32();
                timeBase = reader.ReadUInt64();
                timeFreq = reader.ReadUInt64();

                if ( version == 2 )
                {
                    engineInstanceId = reader.ReadUInt64();
                    timeBase = reader.ReadUInt64();
                }
            }
            catch ( Exception )
            {
                return null;
            }

            // load entries
            ProfilerData ret = new ProfilerData( path, timeBase, timeFreq, engineInstanceId );
            uint numMessages = 0;
            try
            {
                List<RawEvent> openedBlocks = new List<RawEvent>();

                while ( reader.BaseStream.Position < reader.BaseStream.Length )
                {
                    // read message type
                    MessageType type = (MessageType)reader.ReadUInt16();
                    numMessages = numMessages + 1;

                    // read related message ID
                    UInt16 id = reader.ReadUInt16();

                    // process definitions
                    if ( type == MessageType.DefString )
                    {
                        uint hash = reader.ReadUInt32();
                        string txt = LoadString( reader );

                        // do not add twice
                        if ( !ret.Strings.ContainsKey( hash ) )
                        {
                            ret.Strings.Add( hash, txt );
                        }

                        continue;
                    }
                    else if ( type == MessageType.DefCounter || type == MessageType.DefSignal || type == MessageType.DefBlock )
                    {
                        byte numStartParams = reader.ReadByte();
                        byte numEndParams = reader.ReadByte();
                        string name = LoadString( reader );

                        // do not add twice
                        if ( type == MessageType.DefBlock )
                        {
                            ret.Blocks.Add( id, new ProfilerDefinition( id, name, ProfilerDefinitionType.Block, numStartParams, numEndParams ) );
                        }
                        else if ( type == MessageType.DefCounter )
                        {
                            ret.Counters.Add( id, new ProfilerDefinition( id, name, ProfilerDefinitionType.Counter, numStartParams, numEndParams ) );
                        }
                        else if ( type == MessageType.DefSignal )
                        {
                            ret.Signals.Add( id, new ProfilerDefinition( id, name, ProfilerDefinitionType.Signal, numStartParams, numEndParams ) );
                        }

                        continue;
                    }

                    // load common data
                    uint threadID = reader.ReadUInt32();
                    ulong time = reader.ReadUInt64();

                    // parse data
                    ProfilerDefinition evtDef;
                    RawEventType evtType;
                    uint[] evtData;
                    if ( type == MessageType.StartBlock )
                    {
                        evtDef = ret.Blocks[id]; // get by block ID
                        evtData = LoadData( reader, evtDef.NumParamsStart );
                        evtType = RawEventType.BlockStart;
                    }
                    else if ( type == MessageType.EndBlock )
                    {
                        evtDef = ret.Blocks[id]; // get by block ID
                        evtData = LoadData( reader, evtDef.NumParamsEnd );
                        evtType = RawEventType.BlockEnd;
                    }
                    else if ( type == MessageType.IncrementCounter )
                    {
                        evtDef = ret.Counters[id]; // get by counter ID
                        evtData = LoadData( reader, evtDef.NumParamsStart );
                        evtType = RawEventType.CounterInc;
                    }
                    else if ( type == MessageType.DecrementCounter )
                    {
                        evtDef = ret.Counters[id]; // get by counter ID
                        evtData = LoadData( reader, evtDef.NumParamsStart );
                        evtType = RawEventType.CounterDec;
                    }
                    else if ( type == MessageType.Signal )
                    {
                        evtDef = ret.Signals[id]; // get by signal ID
                        evtData = LoadData( reader, evtDef.NumParamsStart );
                        evtType = RawEventType.Signal;
                    }
                    else
                    {
                        throw new Exception();
                    }

                    // add event
                    ProfilerThread thread = ret.AddThread( threadID );
                    RawEvent evt = new RawEvent( time, evtDef, evtType, evtData, thread );

                    // glue blocks
                    if ( type == MessageType.StartBlock )
                    {
                        openedBlocks.Add( evt );
                    }
                    else if ( type == MessageType.EndBlock )
                    {
                        RawEvent matchedEvent = null;
                        foreach ( var b in openedBlocks )
                        {
                            if ( (b.Thread == evt.Thread) &&
                                (b.Def == evt.Def) )
                            {
                                matchedEvent = b;
                            }
                        }

                        // connect best
                        if ( matchedEvent != null )
                        {
                            matchedEvent.LinkEnd( evt );
                            openedBlocks.Remove( matchedEvent );
                        }
                        else
                        {
                            // unmatched block event, do not add it to internal list
                            continue;
                        }
                    }

                    // add to list
                    ret.AddEvent( evt );
                }
            }
            catch ( Exception )
            {
                return null;
            }

            // data loaded
            return ret;
        }

        private string LoadString( BinaryReader reader )
        {
            int length = reader.ReadInt32();
            byte[] data = reader.ReadBytes( length );
            return System.Text.Encoding.UTF8.GetString( data );
        }

        private uint[] LoadData( BinaryReader reader, int numParms )
        {
            if ( numParms == 0 )
                return null;

            uint[] data = new uint[numParms];
            for ( int i = 0; i < numParms; ++i )
            {
                data[i] = reader.ReadUInt32();
            }
            return data;
        }

        private enum MessageType
        {
            DefString = 1,
            DefBlock = 2,
            DefSignal = 3,
            DefCounter = 4,

            ThreadName = 10,
            Signal = 11,
            StartBlock = 12,
            EndBlock = 13,
            IncrementCounter = 14,
            DecrementCounter = 15,
        }
    }
}
